/*
*    l3qd - Light, light, lightweight queue
*    Copyright (C) 2024  Marcus Pedersén marcus@marcux.org
*
*    This program is free software: you can redistribute it and/or modify
*    it under the terms of the GNU General Public License as published by
*    the Free Software Foundation, either version 3 of the License, or
*    (at your option) any later version.
*
*    This program is distributed in the hope that it will be useful,
*    but WITHOUT ANY WARRANTY; without even the implied warranty of
*    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*    GNU General Public License for more details.
*
*    You should have received a copy of the GNU General Public License
*    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package sql

import (
	"errors"
	"fmt"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
	"notabug.org/marcux/l3q/internal/common"
	"notabug.org/marcux/l3q/internal/daemon"
	"strconv"
	"strings"
)

type Client struct {
	Id        int
	Ip        string
	Name      string
	Hostname  string
	Key       string
	DaemonKey string
}

// Initializes database and
// returns database handle
func InitDb(c daemon.Config) (*gorm.DB, error) {
	db, err := gorm.Open(sqlite.Open(c.DbFile), &gorm.Config{})
	if err != nil {
		return &gorm.DB{}, fmt.Errorf("Failed to open database: %s, %s", c.DbFile, err)
	}

	db.Exec("pragma journal_mode=wal;")
	if db.Error != nil {
		return &gorm.DB{}, fmt.Errorf("Failed to set journal_mode=wal, %s", db.Error)
	}

	err = db.AutoMigrate(&common.Node{})
	if err != nil {
		return &gorm.DB{}, fmt.Errorf("Failed to create table for Node, %s", err)
	}

	err = db.AutoMigrate(&common.Job{})
	if err != nil {
		return &gorm.DB{}, fmt.Errorf("Failed to create table for Job, %s", err)
	}

	err = db.AutoMigrate(&common.Task{})
	if err != nil {
		return &gorm.DB{}, fmt.Errorf("Failed to create table for Task, %s", err)
	}

	err = db.AutoMigrate(&Client{})
	if err != nil {
		return &gorm.DB{}, fmt.Errorf("Failed to create table for Client, %s", err)
	}

	return db, nil
}

// Get jobs
// If id is positive integer
// specific job is returned
// with tasks if it is set.
// If id is a negative integer
// either queue or history can
// be set and relevant jobs will
// be returned.
// If limit is larger then 0, limits
// number of items returned. Can only
// be combined with history.
// Any other argument combinations
// will return an error.
func GetJob(db *gorm.DB, id int, tasks bool, state common.JobState, queue bool, history bool, limit int) ([]common.Job, error) {
	js := []common.Job{}
	j := common.Job{}

	if id <= 0 {
		if queue && !history {
			db.Where("state <= ?", int(common.JobStateCancel)).Find(&js)
			if db.Error != nil {
				return js, fmt.Errorf("Failed to get all queued jobs from database: %s", db.Error)
			}
		} else if history && !queue {
			if limit > 0 {
				db.Limit(limit).Order("end_date desc, id desc").Where("state > ?", int(common.JobStateCancel)).Find(&js)
				if db.Error != nil {
					return js, fmt.Errorf("Failed to get %d number of history jobs from database: %s", limit, db.Error)
				}
			} else {
				db.Order("end_date desc, id desc").Where("state > ?", int(common.JobStateCancel)).Find(&js)
				if db.Error != nil {
					return js, fmt.Errorf("Failed to get all history jobs from database: %s", db.Error)
				}
			}
		} else if history && queue {
			return js, fmt.Errorf("Wrong arguments to function sql.GetJob, queue and history can not both be true")
		} else if state.IsValid() {
			if tasks {
				result := db.Model(&j).Preload("Tasks").Order("init_date").Where("state = ?", state).Find(&js)
				if errors.Is(result.Error, gorm.ErrRecordNotFound) {
					return js, nil
				} else if db.Error != nil {
					return js, fmt.Errorf("Failed to get all job with state: %s including all tasks from database: %s", state.String(), db.Error)
				}
			} else {
				db.Where("state = ?", state).Find(&js)
				if db.Error != nil {
					return js, fmt.Errorf("Failed to get all jobs with state: %s from database: %s", state, db.Error)
				}
			}
		} else {
			db.Find(&js)
			if db.Error != nil {
				return js, fmt.Errorf("Failed to get all jobs from database: %s", db.Error)
			}
		}
	} else {
		if tasks {
			result := db.Model(&j).Preload("Tasks").First(&j, id)
			if errors.Is(result.Error, gorm.ErrRecordNotFound) {
				return js, nil
			} else if db.Error != nil {
				return js, fmt.Errorf("Failed to get job with id: %d  including all tasks from database: %s", id, db.Error)
			}
		} else {
			result := db.First(&j, id)
			if errors.Is(result.Error, gorm.ErrRecordNotFound) {
				return js, nil
			} else if result.Error != nil {
				return js, fmt.Errorf("Failed to get job with id: %d from database: %s", id, db.Error)
			}
		}

		js = append(js, j)
	}

	return js, nil
}

// Returns error on failure,
// on error bool is set to true if
// error should be reported to client
// if returned bool is false this is an InternalServerError
// id is jobid that should be cancelled
// user must be the user of the job
// to be able to cancel.
// If admin is true user check is ignored
func CancelJob(db *gorm.DB, id int, user string, admin bool) (bool, error) {
	j := common.Job{}
	authUser := false

	result := db.First(&j, id)
	if errors.Is(result.Error, gorm.ErrRecordNotFound) {
		return true, fmt.Errorf("No record found with job id: %d", id)
	} else if result.Error != nil {
		return false, fmt.Errorf("Failed to get job with id: %d from database: %s", id, result.Error)
	}

	if admin {
		authUser = true
	} else {
		if strings.TrimSpace(j.User) == strings.TrimSpace(user) {
			authUser = true
		}
	}

	if authUser && j.State <= common.JobStateCancel {
		j.State = common.JobStateCancel
		result := db.Save(&j)
		if result.Error != nil {
			return false, fmt.Errorf("Failed to set status cancel on job with id: %d in database: %s", id, result.Error)
		}
	} else {
		if !authUser && j.State > common.JobStateCancel {
			return true, fmt.Errorf("Wrong user specified: %s, not owner of job. JobState: %s is not possible to cancel.", user, j.State.String())
		} else if !authUser {
			return true, fmt.Errorf("Wrong user specified: %s, not owner of job.", user)
		} else if j.State > common.JobStateCancel {
			return true, fmt.Errorf("JobState: %s is not possible to cancel.", j.State.String())
		}
	}

	return false, nil
}

// Returns error on failure.
// Returned int is 0 (zero) if error should
// be reported to user, -1 on internal error.
// If job inserted correct id is returned in int.
// Job is job that should be saved
// to database
func InsertJob(db *gorm.DB, job common.Job) (int, error) {
	job.Id = 0

	ds := strings.Split(job.Depend, ",")

	if len(ds) > 0 && !(len(ds) == 1 && len(ds[0]) == 0) {
		deps := ""
		for _, d := range ds {
			j := common.Job{}
			dId, err := strconv.Atoi(strings.TrimSpace(d))

			if err != nil {
				return 0, fmt.Errorf("Depend is invalid, must be a comma separated string of integers.")
			}

			result := db.First(&j, dId)
			if errors.Is(result.Error, gorm.ErrRecordNotFound) {
				return 0, fmt.Errorf("Depending job with id: %d does not exist", dId)
			} else if result.Error != nil {
				return -1, fmt.Errorf("Failed to get job with id: %d from database, %s", dId, result.Error)
			}

			if len(deps) == 0 {
				deps = fmt.Sprintf("%d", dId)
			} else {
				deps = fmt.Sprintf("%s,%d", deps, dId)
			}
		}

		job.Depend = deps
	}

	result := db.Save(&job)
	if result.Error != nil {
		return -1, fmt.Errorf("Failed to insert job: %v into database: %s", job, result.Error)
	}

	return job.Id, nil
}

// Update existing job
// id is jobid that should be updated
// j is node struct with values that
// should be updated, all non zero values
func UpdateJob(db *gorm.DB, id int, j common.Job) error {
	jobId := common.Job{Id: id}

	result := db.Model(&jobId).Updates(j)
	if result.Error != nil {
		return fmt.Errorf("Failed to update job with id: %d, in database: %s", id, result.Error)
	}

	return nil
}

// Returns task from database
// If id is specified specific
// task is returned.
// If jobid is specified all tasks
// belonging to that job is returned.
// If running is specified all running
// tasks are returned.
// If id and jobid is less than zero
// and running is false then all tasks
// are returned.
// On error an error is returned.
func GetTask(db *gorm.DB, id int, jobId int, running bool) ([]common.Task, error) {
	ts := []common.Task{}
	t := common.Task{}

	if id > 0 {
		result := db.First(&t, id)
		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
			return ts, nil
		} else if result.Error != nil {
			return ts, fmt.Errorf("Failed to get task with id: %d from database: %s", id, result.Error)
		}

		ts = append(ts, t)
	} else if jobId > 0 {
		db.Where("job_id = ?", jobId).Find(&ts)

		if db.Error != nil {
			if errors.Is(db.Error, gorm.ErrRecordNotFound) {
				return ts, fmt.Errorf("No tasks found for jobid: %d", jobId)
			} else {
				return ts, fmt.Errorf("Failed to get all tasks belonging to jobid: %d from database: %s", jobId, db.Error)
			}
		}
	} else if running {
		db.Where("sub_state != ? AND sub_state != ?", "exited", "failed").Find(&ts)

		if db.Error != nil {
			if errors.Is(db.Error, gorm.ErrRecordNotFound) {
				return ts, fmt.Errorf("No running tasks found")
			} else {
				return ts, fmt.Errorf("Failed to get all running tasks from database: %s", db.Error)
			}
		}
	} else {
		db.Find(&ts)
	}

	return ts, nil
}

// Returns node from database
// If id is specified specific
// node is returned.
// If tasks is specified all nodes
// and tasks running on each node
// are returned.
// If state is valid, all nodes with
// that state is returned.
// If id is less than zero  and state is invalid
// and tasks is false then all nodes
// are returned.
// On error an error is returned.
func GetNode(db *gorm.DB, id int, tasks bool, state common.NodeState) ([]common.Node, error) {
	ns := []common.Node{}
	n := common.Node{}

	if id > 0 {
		if tasks {
			result := db.Model(&n).Preload("Tasks").First(&n, id)
			if errors.Is(result.Error, gorm.ErrRecordNotFound) {
				return ns, nil
			} else if result.Error != nil {
				return ns, fmt.Errorf("Failed to get node with id: %d, including all tasks from database: %s", id, result.Error)
			}

			ns = append(ns, n)
		} else {
			result := db.First(&n, id)
			if errors.Is(result.Error, gorm.ErrRecordNotFound) {
				return ns, nil
			} else if result.Error != nil {
				return ns, fmt.Errorf("Failed to get node with id: %d from database: %s", id, result.Error)
			}

			ns = append(ns, n)
		}
	} else if state.IsValid() {
		if tasks {
			db.Model(&n).Preload("Tasks").Where("state = ?", state).Find(&ns)
			if errors.Is(db.Error, gorm.ErrRecordNotFound) {
				return ns, nil
			} else if db.Error != nil {
				return ns, fmt.Errorf("Failed to get all nodes with state: %s including tasks from database: %s", state.String(), db.Error)
			}
		} else {
			db.Where("state = ?", state).Find(&ns)
			if errors.Is(db.Error, gorm.ErrRecordNotFound) {
				return ns, nil
			} else if db.Error != nil {
				return ns, fmt.Errorf("Failed to get all nodes with status: %s from database: %s", state.String(), db.Error)
			}
		}
	} else {
		if tasks {
			db.Order("name, hostname").Model(&n).Preload("Tasks").Find(&ns)
			if errors.Is(db.Error, gorm.ErrRecordNotFound) {
				return ns, nil
			} else if db.Error != nil {
				return ns, fmt.Errorf("Failed to get all nodes including tasks from database: %s", db.Error)
			}
		} else {
			db.Order("name, hostname").Find(&ns)
			if errors.Is(db.Error, gorm.ErrRecordNotFound) {
				return ns, nil
			} else if db.Error != nil {
				return ns, fmt.Errorf("Failed to get all nodes from database: %s", db.Error)
			}
		}
	}

	return ns, nil
}

// Removes specified node
// from database.
// Returns error on failure.
func RemoveNode(db *gorm.DB, id int) error {
	result := db.Delete(common.Node{}, id)

	if result.RowsAffected < 1 {
		return fmt.Errorf("Node with id: %d does not exist", id)
	} else if result.Error != nil {
		return fmt.Errorf("%s", db.Error)
	}

	return nil
}

// Returns error on failure,
// id is nodeid that should be set offline.
func SetNodeOffline(db *gorm.DB, id int) error {
	n := common.Node{}

	result := db.First(&n, id)
	if errors.Is(result.Error, gorm.ErrRecordNotFound) {
		return fmt.Errorf("No record found with node id: %d", id)
	} else if result.Error != nil {
		return fmt.Errorf("Failed to get node with id: %d from database: %s", id, result.Error)
	}

	n.State = common.NodeStateOffline
	result = db.Save(&n)
	if result.Error != nil {
		return fmt.Errorf("Failed to set state NodeStateOffline on node with id: %d in database: %s", id, result.Error)
	}

	return nil
}

// Returns error on failure,
// on error bool is set to true if
// error should be reported to client
// if returned bool is false this is an InternalServerError
// id is nodeid that should be set online/offline.
// online is true if node should be set to online,
// if false node is set offline
func SetStateNode(db *gorm.DB, id int, online bool) (bool, error) {
	n := common.Node{}

	result := db.First(&n, id)
	if errors.Is(result.Error, gorm.ErrRecordNotFound) {
		return true, fmt.Errorf("No record found with node id: %d", id)
	} else if result.Error != nil {
		return false, fmt.Errorf("Failed to get node with id: %d from database: %s", id, result.Error)
	}

	if online {
		if n.State >= common.NodeStateOffline {
			n.State = common.NodeStateSoftOnline
			result := db.Save(&n)
			if result.Error != nil {
				return false, fmt.Errorf("Failed to set state NodeStateMaintenanceDrain on node with id: %d in database: %s", id, result.Error)
			}
		} else {
			return true, fmt.Errorf("Node with id: %d is in state: %s and can not be changed to state: online", id, n.State.String())
		}
	} else {
		if n.State < common.NodeStateOffline {
			n.State = common.NodeStateMaintenanceDrain
			result = db.Save(&n)
			if result.Error != nil {
				return false, fmt.Errorf("Failed to set state NodeStateSoftOnline on node with id: %d in database: %s", id, result.Error)
			}
		} else {
			return true, fmt.Errorf("Node with id: %d is in state: %s and can not be changed to state: offline", id, n.State.String())
		}
	}

	return false, nil
}

// Insert a newly validated node
func InsertNode(db *gorm.DB, n common.Node) error {
	if n.Id != 0 || len(n.Ip) == 0 || n.Port == 0 || len(n.Key) == 0 || len(n.DaemonKey) == 0 {
		return fmt.Errorf("Failed to insert new node in database, required data is not submitted")
	}

	result := db.Save(&n)

	if result.Error != nil {
		return fmt.Errorf("Failed to insert node with IP: %s, %s", n.Ip, result.Error)
	}

	return nil
}

// Update existing node
// id is nodeid that should be updated
// n is node struct with values that
// should be updated, all non zero values
func UpdateNode(db *gorm.DB, id int, n common.Node) error {
	nodeId := common.Node{Id: id}

	result := db.Model(&nodeId).Updates(n)
	if result.Error != nil {
		return fmt.Errorf("Failed to update node with id: %d, in database: %s", id, result.Error)
	}

	return nil
}

// Returns all client hosts
// On error, error is returned
func GetClient(db *gorm.DB) ([]Client, error) {
	cs := []Client{}

	db.Find(&cs)
	if errors.Is(db.Error, gorm.ErrRecordNotFound) {
		return cs, nil
	} else if db.Error != nil {
		return cs, fmt.Errorf("Failed to get all client nodes from database: %s", db.Error)
	}

	return cs, nil
}

// Removes specified client
// from database.
// Returns error on failure.
func RemoveClient(db *gorm.DB, id int) error {
	result := db.Delete(Client{}, id)

	if result.RowsAffected < 1 {
		return fmt.Errorf("Client with id: %d does not exist", id)
	} else if result.Error != nil {
		return fmt.Errorf("%s", db.Error)
	}

	return nil
}

// Insert a newly validated client
func InsertClient(db *gorm.DB, c Client) error {
	if c.Id != 0 || len(c.Ip) == 0 || len(c.Key) == 0 || len(c.DaemonKey) == 0 {
		return fmt.Errorf("Failed to insert new client in database, required data is not submitted")
	}

	result := db.Save(&c)

	if result.Error != nil {
		return fmt.Errorf("Failed to insert client with IP: %s, %s", c.Ip, result.Error)
	}

	return nil
}

// Returns all clients
// with specified key
func GetClientKey(db *gorm.DB, key string) ([]Client, error) {
	cs := []Client{}

	db.Where("key == ?", key).Find(&cs)
	if db.Error != nil {
		if errors.Is(db.Error, gorm.ErrRecordNotFound) {
			return cs, nil
		} else if db.Error != nil {
			return cs, fmt.Errorf("Failed to get client with specified key from database: %s", db.Error)
		}
	}

	return cs, nil
}

// Returns all nodes
// with specified key
func GetNodeKey(db *gorm.DB, key string) ([]common.Node, error) {
	ns := []common.Node{}

	db.Where("key == ?", key).Find(&ns)
	if db.Error != nil {
		if errors.Is(db.Error, gorm.ErrRecordNotFound) {
			return ns, nil
		} else if db.Error != nil {
			return ns, fmt.Errorf("Failed to get node with specified key from database: %s", db.Error)
		}
	}

	return ns, nil
}
